// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: linchuan

#pragma once

#include <iostream>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <vector>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "coin2/exchange/base/symbology/derivative_util.h"
#include "coin2/exchange/base/symbology/futures_product.h"
#include "coin2/exchange/bitmex/symbology/currency.h"

namespace coin2::exchange::bitmex_futures::symbology {

using BitmexCurrency = bitmex::symbology::BitmexCurrency;

class BitmexFuturesProduct
    : public base::symbology::FuturesProduct<BitmexCurrency, BitmexFuturesProduct>,
      public base::symbology::ProductFromStrMixin<BitmexFuturesProduct> {
 public:
  using FuturesProduct = base::symbology::FuturesProduct<BitmexCurrency, BitmexFuturesProduct>;
  using ContractType = base::symbology::ContractType;

 public:
  BitmexFuturesProduct(
      const BitmexCurrency& base,
      const BitmexCurrency& quote,
      const std::string& native_product,
      const std::string& native_subscription_symbol,
      const ContractType contract_type,
      const int64_t expiry_timestamp,
      const int64_t resolve_timestamp)
      : FuturesProduct(
            base,
            quote,
            native_product,
            native_subscription_symbol,
            base::market::ExchangeType::Bitmex,
            contract_type,
            expiry_timestamp,
            resolve_timestamp) {}

 public:
  static std::unique_ptr<BitmexFuturesProduct> FromNativeStrImpl(
      const std::string& native_product,
      const int64_t resolve_timestamp) {
    std::string native_product_str;
    std::string expiry_str;
    std::string base_str = "";
    std::string quote_str = "";
    if (IsNumberic(native_product.substr(native_product.length() - 2, 2))) {
      // delivery contract
      native_product_str = native_product.substr(0, native_product.length() - 3);
      auto short_expiry_code = native_product.substr(native_product.length() - 3, 3);
      CalculateContractType(short_expiry_code, resolve_timestamp, &expiry_str);
      std::unordered_set<std::string> xbt_symbol_set = {"ADA", "ETH", "XRP", "CNH", "BCH", "EOS", "LTC", "TRX"};
      if (xbt_symbol_set.count(native_product_str) > 0) {
        base_str = native_product_str;
        quote_str = "XBT";
      }

      if (native_product_str == "XBT") {
        base_str = native_product_str;
        quote_str = "USD";
      }

    } else {
      // perpetual contract
      expiry_str = "PERPETUAL";
      native_product_str = native_product;
    }

    if (base_str.empty() && quote_str.empty()) {
      if (native_product_str.find("USDT") != std::string::npos) {
        base_str = native_product_str.substr(0, native_product_str.length() - 4);
        quote_str = "USDT";
      } else if (native_product_str.find("USD") != std::string::npos) {
        base_str = native_product_str.substr(0, native_product_str.length() - 3);
        quote_str = "USD";
      } else if (native_product_str.find("EUR") != std::string::npos) {
        base_str = native_product_str.substr(0, native_product_str.length() - 3);
        quote_str = "EUR";
      } else {
        // default quote is USD
        base_str = native_product_str;
        quote_str = "USD";
      }
    }

    const auto base = BitmexCurrency::FromNativeStrNoThrow(base_str, resolve_timestamp);
    const auto quote = BitmexCurrency::FromNativeStrNoThrow(quote_str, resolve_timestamp);
    if (!base) {
      return {};
    }

    std::string native_product_tmp;
    std::string native_subscription_symbol;
    ContractType contract_type;
    int64_t expiry_timestamp;
    bool success = CalculateParams(
        *base,
        *quote,
        expiry_str,
        resolve_timestamp,
        &native_product_tmp,
        &native_subscription_symbol,
        &contract_type,
        &expiry_timestamp);
    if (success) {
      DCHECK_EQ(native_product_tmp, native_product);
      return std::make_unique<BitmexFuturesProduct>(
          *base,
          *quote,
          native_product,
          native_subscription_symbol,
          contract_type,
          expiry_timestamp,
          resolve_timestamp);
    } else {
      return {};
    }
  }

  static bool IsNumberic(const std::string& str) {
    try {
      std::stoi(str);
      return true;
    } catch (const std::exception&) {
    }
    return false;
  }

  static char MonthToCode(int month) {
    const char* month_code_arr = "FGHJKMNQUVXZ";
    return month_code_arr[month - 1];
  }
  static void GetMonthYearFromTimestamp(const int64_t& timestamp, int* year, int* month) {
    using namespace boost::posix_time;

    const auto dt = from_time_t(::ceil(timestamp / 1e9));
    *year = static_cast<int>(dt.date().year());
    *month = static_cast<int>(dt.date().month());
  }

  static bool CalculateParams(
      const BitmexCurrency& base,
      const BitmexCurrency& quote,
      const std::string& expiry_str,
      const int64_t resolve_timestamp,
      std::string* native_product,
      std::string* native_subscription_symbol,
      ContractType* contract_type,
      int64_t* expiry_timestamp) {
    using base::symbology::ContractType;
    using base::symbology::ExchangeType;
    using base::symbology::FindExpiryFriday;
    using base::symbology::IsDateStr;
    using base::symbology::StringToContractType;
    using base::symbology::ToDateStr;

    if (IsDateStr(expiry_str)) {
      bool found = false;
      for (auto type : {ContractType::WEEK,
                        ContractType::NEXT_WEEK,
                        ContractType::QUARTER,
                        ContractType::NEXT_QUARTER,
                        ContractType::NNEXT_QUARTER,
                        ContractType::NNNEXT_QUARTER}) {
        auto timestamp = FindExpiryFriday(
            ExchangeType::Bitmex,
            type,
            resolve_timestamp,
            expiry_time_of_day_nanosec);
        if (expiry_str == ToDateStr(timestamp) || expiry_str == ToDateStr(timestamp).substr(4)) {
          *contract_type = type;
          *expiry_timestamp = timestamp;
          found = true;
          break;
        }
      }
      if (!found) {
        return false;
      }
    } else {
      auto type = StringToContractType(expiry_str);
      if (type) {
        *contract_type = type.value();
        if (*contract_type == ContractType::PERPETUAL) {
          *expiry_timestamp = FuturesProduct::PERPETUAL_TIMESTAMP;
        } else if (*contract_type == ContractType::WEEK) {
          NOTREACHED();
        } else if (*contract_type == ContractType::QUARTER) {
          *expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
        } else if (*contract_type == ContractType::NEXT_QUARTER) {
          *expiry_timestamp = FindExpiryNextQuarter(resolve_timestamp);
        } else if (*contract_type == ContractType::NNEXT_QUARTER) {
          *expiry_timestamp = FindExpiryNNextQuarter(resolve_timestamp);
        } else if (*contract_type == ContractType::NNNEXT_QUARTER) {
          *expiry_timestamp = FindExpiryNNNextQuarter(resolve_timestamp);
        } else {
          NOTREACHED() << base.symbol() << ", " << quote.symbol() << ", " << expiry_str;
        }
      } else {
        return false;
      }
    }

    std::stringstream ss;
    if (*contract_type == ContractType::PERPETUAL) {
      ss << base.native_currency() << quote.native_currency();
    } else {
      using namespace boost::posix_time;

      auto expiry_date = from_time_t(::ceil(*expiry_timestamp / 1e9)).date();
      if (quote.currency() == "USDT") {
        if (base.native_currency() == "ETHPOW") {
          ss << base.native_currency() << MonthToCode(expiry_date.month())
             << std::to_string(expiry_date.year() % 100);
        } else {
          ss << base.native_currency() << quote.native_currency()
             << MonthToCode(expiry_date.month()) << std::to_string(expiry_date.year() % 100);
        }
      } else if (quote.currency() == "BTC") {
        ss << base.native_currency() << MonthToCode(expiry_date.month())
           << std::to_string(expiry_date.year() % 100);
      } else if (quote.currency() == "USD") {
        if (base.currency() == "BTC") {
          ss << base.native_currency() << MonthToCode(expiry_date.month())
             << std::to_string(expiry_date.year() % 100);
        } else {
          ss << base.native_currency() << quote.native_currency()
             << MonthToCode(expiry_date.month()) << std::to_string(expiry_date.year() % 100);
        }
      } else if (quote.currency() == "EUR") {
        ss << base.native_currency() << quote.native_currency() << MonthToCode(expiry_date.month())
           << std::to_string(expiry_date.year() % 100);
      } else {
        LOG(INFO) << "Not Support: "<< base.currency() << "-" << quote.currency() << "." << expiry_str;
        return false;
      }
    }
    *native_product = ss.str();
    *native_subscription_symbol = *native_product;
    return true;
  }

  static int64_t FindExpiryQuarter(const int64_t resolve_timestamp) {
    using namespace coin2::exchange::base::symbology;
    return FindExpiryQuarterlyLastFriday(ExchangeType::Bitmex,resolve_timestamp,expiry_time_of_day_nanosec);
  }

  static int64_t FindExpiryNextQuarter(const int64_t resolve_timestamp) {
    int64_t quarter_expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
    return FindExpiryQuarter(quarter_expiry_timestamp + 24 * 3600 * 1e9);
  }

  static int64_t FindExpiryNNextQuarter(const int64_t resolve_timestamp) {
    auto ts = FindExpiryNextQuarter(resolve_timestamp);
    return FindExpiryQuarter(ts + 24 * 3600 * 1e9);
  }

  static int64_t FindExpiryNNNextQuarter(const int64_t resolve_timestamp) {
    auto ts = FindExpiryNNextQuarter(resolve_timestamp);
    return FindExpiryQuarter(ts + 24 * 3600 * 1e9);
  }

  static bool CalculateContractType(
      const std::string& short_expiry_code,
      const int64_t resolve_timestamp,
      std::string* expiry_str) {
    using base::symbology::ContractType;

    int64_t quarter_expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
    int quarter_month;
    int quarter_year;
    GetMonthYearFromTimestamp(quarter_expiry_timestamp, &quarter_year, &quarter_month);
    std::string calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
    if (calc_code == short_expiry_code) {
      *expiry_str = "QUARTER";
      return true;
    } else {
      int64_t next_quarter_expiry_timestamp = FindExpiryNextQuarter(resolve_timestamp);
      GetMonthYearFromTimestamp(next_quarter_expiry_timestamp, &quarter_year, &quarter_month);
      calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
      if (calc_code == short_expiry_code) {
        *expiry_str = "NEXT_QUARTER";
        return true;
      } else {
        int64_t nnext_quarter_expiry_timestamp = FindExpiryNNextQuarter(resolve_timestamp);
        GetMonthYearFromTimestamp(nnext_quarter_expiry_timestamp, &quarter_year, &quarter_month);
        calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
        if (calc_code == short_expiry_code) {
          *expiry_str = "NNEXT_QUARTER";
          return true;
        } else {
          int64_t nnnext_quarter_expiry_timestamp = FindExpiryNNNextQuarter(resolve_timestamp);
          GetMonthYearFromTimestamp(nnnext_quarter_expiry_timestamp, &quarter_year, &quarter_month);
          calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
          if (calc_code == short_expiry_code) {
            *expiry_str = "NNNEXT_QUARTER";
            return true;
          }
        }
      }
    }
    return false;
  }

  static const int64_t expiry_time_of_day_nanosec = 12 * 3600 * 1e9;
};

}  // namespace coin2::exchange::bitmex_futures::symbology
